Lectura de los datos

datos <- read.csv(file="googleplaystore.csv", header=TRUE, sep=",", stringsAsFactors = FALSE)
num <- c(1:10841)
apps <- c(datos$Category)
number_of_apps <- data.frame(num, apps)

Echamos un vistazo al dataset

str(datos)
## 'data.frame':    10841 obs. of  13 variables:
##  $ App           : chr  "Photo Editor & Candy Camera & Grid & ScrapBook" "Coloring book moana" "U Launcher Lite â\200“ FREE Live Cool Themes, Hide Apps" "Sketch - Draw & Paint" ...
##  $ Category      : chr  "ART_AND_DESIGN" "ART_AND_DESIGN" "ART_AND_DESIGN" "ART_AND_DESIGN" ...
##  $ Rating        : num  4.1 3.9 4.7 4.5 4.3 4.4 3.8 4.1 4.4 4.7 ...
##  $ Reviews       : chr  "159" "967" "87510" "215644" ...
##  $ Size          : chr  "19M" "14M" "8.7M" "25M" ...
##  $ Installs      : chr  "10,000+" "500,000+" "5,000,000+" "50,000,000+" ...
##  $ Type          : chr  "Free" "Free" "Free" "Free" ...
##  $ Price         : chr  "0" "0" "0" "0" ...
##  $ Content.Rating: chr  "Everyone" "Everyone" "Everyone" "Teen" ...
##  $ Genres        : chr  "Art & Design" "Art & Design;Pretend Play" "Art & Design" "Art & Design" ...
##  $ Last.Updated  : chr  "January 7, 2018" "January 15, 2018" "August 1, 2018" "June 8, 2018" ...
##  $ Current.Ver   : chr  "1.0.0" "2.0.0" "1.2.4" "Varies with device" ...
##  $ Android.Ver   : chr  "4.0.3 and up" "4.0.3 and up" "4.0.3 and up" "4.2 and up" ...

Limpieza de datos y transformación

El primer paso será hacer un preprocesamiento de cada uno de los atributos con vista a obtener mejores resultados en los algoritmos de minería de datos

Size

Se puede obsrevar que los datos del tamaño de las aplicaciones tienen prefijos métricos (Kilo y Mega). Para poder hacer un análisis de datos efectivo, se eliminarán estos símbolos por sus correspondientes equivalencias numéricas. Además de estos símbolos hay apps cuyo tamaño varían con el dispositivo (“Varies with devices”), estos ejemplos se sustituirán por valores nulos. Por otro lado, también hay instancias que tienen como valor “1000+”, estas se sustituirán por 1000. En resumen, las tareas que se van a realizar son las siguientes:

  • Reemplazar “Varies with devices” por NaN
  • Convertir k y M a numérico
  • Pasar 1.000+ a 1000 para hacerlo numérico
  • Los valores NaN se sustituyen por la media de la columna
for (i in 1:length(datos$Size)) { 
  if(grepl("M",datos$Size[i])){
    numero <- as.numeric(gsub("M", "e+06", datos$Size[i]))
    datos$Size[i]<- numero
  } else if (grepl("k", datos$Size[i])){
    numero <- as.numeric(gsub("k", "e+03", datos$Size[i]))
    datos$Size[i] <- numero
  }else if (grepl("Varies", datos$Size[i])){
    datos$Size[i] <- NaN
  }else{
    datos$Size[i]<-1000
  }
}

datos$Size <- as.numeric(datos$Size)
datos$Size[is.na(datos$Size)] <- mean(datos$Size, na.rm=TRUE)

Installs

Convertir “Installs” en numeric Removemos el simbolo (+) y luego convertimos a numérico. Comprobamos los cambios.

datos$Installs <- as.numeric(gsub(",", "", gsub("+", "", datos$Installs, fixed = TRUE), fixed=TRUE))
options("scipen"=100, "digits"=4)
str(datos$Installs)
##  num [1:10841] 10000 500000 5000000 50000000 100000 50000 50000 1000000 1000000 10000 ...
print(unique(datos$Installs))
##  [1]      10000     500000    5000000   50000000     100000      50000
##  [7]    1000000   10000000       5000  100000000 1000000000       1000
## [13]  500000000         50        100        500         10          1
## [19]          5          0         NA
##Convertir Nan en 0 porque el Nan viene de una app que es Free
for (i in 1:length(datos$Installs)) { 
  if(is.na(datos$Installs[i])){
    datos$Installs[i]<-0
  }
}

Reviews

Comprobaremos si los valores del atributo “Reviews” son de tipo numérico:

datos$Reviews <- as.numeric(datos$Reviews)
print(sum(is.na(datos$Reviews)))
## [1] 1

Se puede observar que al convertir la columna a número, un valor no se ha podido convertir, ya que no había forma. Daremos un vistazo previo a esta fila:

for (i in 1:length(datos$Reviews)) {
  if(is.na(datos$Reviews[i])){
    print(i)
    print(datos[i,])
    datos <- datos[-i,]
    
  }
}
## [1] 10473
##                                           App Category Rating Reviews Size
## 10473 Life Made WI-Fi Touchscreen Photo Frame      1.9     19      NA 1000
##       Installs Type    Price Content.Rating            Genres Last.Updated
## 10473        0    0 Everyone                February 11, 2018       1.0.19
##       Current.Ver Android.Ver
## 10473  4.0 and up            
## [1] 10841
##     App Category Rating Reviews Size Installs Type Price Content.Rating
## NA <NA>     <NA>     NA      NA   NA       NA <NA>  <NA>           <NA>
##    Genres Last.Updated Current.Ver Android.Ver
## NA   <NA>         <NA>        <NA>        <NA>

Como solo es una fila, se optará por eliminarla directamente. En la representación de arriba se ve cómo ha desaparecido del dataframe.

Rating

Se comprueba que los valores están entre 1 y 5. Tiene valores que son NaN, se sustituyen por la media de la columna

print(range(datos$Rating, na.rm = TRUE))
## [1] 1 5
datos$Rating[is.na(datos$Rating)] <- mean(datos$Rating, na.rm=TRUE)

Price

print(unique(datos$Price))
##  [1] "0"       "$4.99"   "$3.99"   "$6.99"   "$1.49"   "$2.99"   "$7.99"  
##  [8] "$5.99"   "$3.49"   "$1.99"   "$9.99"   "$7.49"   "$0.99"   "$9.00"  
## [15] "$5.49"   "$10.00"  "$24.99"  "$11.99"  "$79.99"  "$16.99"  "$14.99" 
## [22] "$1.00"   "$29.99"  "$12.99"  "$2.49"   "$10.99"  "$1.50"   "$19.99" 
## [29] "$15.99"  "$33.99"  "$74.99"  "$39.99"  "$3.95"   "$4.49"   "$1.70"  
## [36] "$8.99"   "$2.00"   "$3.88"   "$25.99"  "$399.99" "$17.99"  "$400.00"
## [43] "$3.02"   "$1.76"   "$4.84"   "$4.77"   "$1.61"   "$2.50"   "$1.59"  
## [50] "$6.49"   "$1.29"   "$5.00"   "$13.99"  "$299.99" "$379.99" "$37.99" 
## [57] "$18.99"  "$389.99" "$19.90"  "$8.49"   "$1.75"   "$14.00"  "$4.85"  
## [64] "$46.99"  "$109.99" "$154.99" "$3.08"   "$2.59"   "$4.80"   "$1.96"  
## [71] "$19.40"  "$3.90"   "$4.59"   "$15.46"  "$3.04"   "$4.29"   "$2.60"  
## [78] "$3.28"   "$4.60"   "$28.99"  "$2.95"   "$2.90"   "$1.97"   "$200.00"
## [85] "$89.99"  "$2.56"   "$30.99"  "$3.61"   "$394.99" "$1.26"   "$1.20"  
## [92] "$1.04"

Se puede observar que la variable precio tiene un carácter $ que hay que eliminar para poder convertirlo en número. Además, hay varias columnas que tienen valores raros (“Everyone”), estas filas las convertiremos en Nan y posteriormente se sustituirá por la media de precios

for (i in 1:length(datos$Price)){
  datos$Price[i] <- as.numeric((gsub("\\$","", datos$Price[i])))
}

datos$Price[is.na(datos$Price)] <- mean(datos$Price, na.rm=TRUE)
datos$Price <- as.numeric(datos$Price)
#Eliminar valores NAN
# for (i in 1:length(datos$Price)){
#   if(is.na(datos$Price[i])){
#     print(i)
#     print(datos[i,])
#     datos <- datos[-i,]
#     
#   }
# }

Lo más curioso es que hay aplicaciones que superan los 350 dólares, tal como se puede ver en el histograma a continuación:

hist(datos$Price)

for (i in 1:length(datos$Price)){
  if(datos$Price[i]>350){
    print(datos$Price[i])
  }
}
## [1] 400
## [1] 400
## [1] 400
## [1] 400
## [1] 400
## [1] 400
## [1] 380
## [1] 400
## [1] 400
## [1] 400
## [1] 400
## [1] 390
## [1] 400
## [1] 400
## [1] 395
## [1] 400

Genres

Esta columna tiene algunos datos que están en el formato Category;Subcategory para poder hacer un estudio más exhaustivo, se va a dividir esta columna en dos, por un lado una columna con la Categoría principal y otra con la subcategoría. Luego comprobamos valores unicos.

head(datos$Genres, n = 50)
##  [1] "Art & Design"                    "Art & Design;Pretend Play"      
##  [3] "Art & Design"                    "Art & Design"                   
##  [5] "Art & Design;Creativity"         "Art & Design"                   
##  [7] "Art & Design"                    "Art & Design"                   
##  [9] "Art & Design"                    "Art & Design;Creativity"        
## [11] "Art & Design"                    "Art & Design"                   
## [13] "Art & Design"                    "Art & Design"                   
## [15] "Art & Design"                    "Art & Design"                   
## [17] "Art & Design"                    "Art & Design"                   
## [19] "Art & Design"                    "Art & Design"                   
## [21] "Art & Design"                    "Art & Design"                   
## [23] "Art & Design"                    "Art & Design;Action & Adventure"
## [25] "Art & Design"                    "Art & Design"                   
## [27] "Art & Design;Creativity"         "Art & Design"                   
## [29] "Art & Design"                    "Art & Design"                   
## [31] "Art & Design"                    "Art & Design"                   
## [33] "Art & Design"                    "Art & Design"                   
## [35] "Art & Design"                    "Art & Design"                   
## [37] "Art & Design;Creativity"         "Art & Design"                   
## [39] "Art & Design"                    "Art & Design"                   
## [41] "Art & Design"                    "Art & Design"                   
## [43] "Art & Design"                    "Art & Design;Creativity"        
## [45] "Art & Design"                    "Art & Design"                   
## [47] "Art & Design"                    "Art & Design"                   
## [49] "Art & Design"                    "Auto & Vehicles"
datos <-separate(data=datos, col = Genres, into = c("Pri_Genre", "Sec_Genre"), sep = ";")
head(datos$Pri_Genre, n = 50)
##  [1] "Art & Design"    "Art & Design"    "Art & Design"   
##  [4] "Art & Design"    "Art & Design"    "Art & Design"   
##  [7] "Art & Design"    "Art & Design"    "Art & Design"   
## [10] "Art & Design"    "Art & Design"    "Art & Design"   
## [13] "Art & Design"    "Art & Design"    "Art & Design"   
## [16] "Art & Design"    "Art & Design"    "Art & Design"   
## [19] "Art & Design"    "Art & Design"    "Art & Design"   
## [22] "Art & Design"    "Art & Design"    "Art & Design"   
## [25] "Art & Design"    "Art & Design"    "Art & Design"   
## [28] "Art & Design"    "Art & Design"    "Art & Design"   
## [31] "Art & Design"    "Art & Design"    "Art & Design"   
## [34] "Art & Design"    "Art & Design"    "Art & Design"   
## [37] "Art & Design"    "Art & Design"    "Art & Design"   
## [40] "Art & Design"    "Art & Design"    "Art & Design"   
## [43] "Art & Design"    "Art & Design"    "Art & Design"   
## [46] "Art & Design"    "Art & Design"    "Art & Design"   
## [49] "Art & Design"    "Auto & Vehicles"
head(datos$Sec_Genre) 
## [1] NA             "Pretend Play" NA             NA            
## [5] "Creativity"   NA

Last updated

Convertir la fecha que está en formato String a Date

Sys.setlocale("LC_TIME", "C")
## [1] "C"
datos$Last.Updated <- as.Date(datos$Last.Updated, format = "%B %d, %Y",origin='1970-01-01')

Current version

Convertir versiones a números con el formato número.número

for (i in 1:length(datos$Current.Ver)){
  if(datos$Current.Ver[i]!="Varies with device"){
    datos$Current.Ver[i]<-as.numeric(substr(as.character(datos$Current.Ver[i]),0,3))
  }
} 

Reemplazar los valores nulos con “Varies with device”

for (i in 1:length(datos$Current.Ver)){
  if(is.na(datos$Current.Ver[i])){
    datos$Current.Ver[i]<-"Varies with device"
  }
}

Visualización de los datos

Android Market Breakdown

Aqui veremos cuales de las aplicaciones son mas utilizadas por los usuarios. Para esto tenemos que crear un nuevo data frame con la categoria de aplicaciones mas utilizadas.

p <- plot_ly(number_of_apps, labels = ~apps, values = ~num, type = 'pie') %>%
  layout(title = 'Aplicaciones mas utilizadas segun categoria',
         xaxis = list(showgrid = FALSE, zeroline = FALSE, showticklabels = FALSE),
         yaxis = list(showgrid = FALSE, zeroline = FALSE, showticklabels = FALSE))
p

Pairplot

Este tipo de gráficos permite ver si hay alguna relación entre dos o más variables, pudiendo observar si hay una relación directa (cuando una variable crece la otra también) o inversa (cuando una crece, la otra decrece). En el siguiente gráfico se presentará este gráfico para las variables numéricas del conjunto de datos

columnas_numericas = c("Rating","Reviews", "Size", "Installs", "Price")
ggpairs(datos[columnas_numericas],
        title="Relations in numeric data")

Los coeficientes de correlación no están cercanos ni a 1 ni a -1, más bien a cero, lo que significa que ninguna variable está a priori relacionada con otra

Puntuación media de las aplicaciones

sprintf("La media de la puntuación es:   %f", mean(datos$Rating))
## [1] "La media de la puntuación es:   4.191757"
p<-ggplot(datos, aes(x=Rating)) + 
  geom_histogram(binwidth = 0.1, fill="red")
p

La media de la calificación es de un 4.17, por lo que en general, los usuarios puntúan muy bien las aplicaciones en la Play Store. Tampoco se ve que haya diferencia de puntuación entre aplicaciones gratuitas y no

sprintf("La media de la puntuación de apps Gratuitas es:   %f", mean(datos$Rating[datos$Type=='Free']))
## [1] "La media de la puntuación de apps Gratuitas es:   4.186933"
sprintf("La media de la puntuación de apps de Pago es:   %f", mean(datos$Rating[datos$Type!='Free']))
## [1] "La media de la puntuación de apps de Pago es:   4.252223"

Mejores categorías

g <- ggplot(datos, aes(datos$Category, datos$Rating)) +
  geom_violin(scale="width") + theme(axis.text.x = element_text(angle = 85, hjust = 1)) + stat_summary(fun.y=mean, geom="point", shape=15, size=1) + scale_color_brewer(palette = "Dark2") + geom_hline(yintercept = mean(datos$Rating), linetype="dashed", color="red", size = 2)
print(g)

*Con los puntos negros se observa la media para cada categoría, y la línea roja indica la media de todas las apps. Sabiendo esto, las 3 mejores categorías son EDUCATION, EVENTS y ART AND DESIGN las tres peores son DATING, TOOLS y VIDEO PLAYERS

Estrategia de precios

¿Cómo afecta el precio de las aplicaciones a su puntuación?

# library
library(ggplot2)
library(ggExtra)
## Warning: package 'ggExtra' was built under R version 3.5.2
# classic plot :
p=ggplot(datos, aes(x=datos$Price, y=datos$Rating)) +
      geom_point() +
      theme(legend.position="none") 
#+ coord_cartesian(xlim=c(0,10))
 
# with marginal histogram
p <- ggMarginal(p, type="density")
 
print(p)

La mayoría de aplicaciones más valoradas se encuentran en el rango de precios de 0 a 50 dólares.

Minería de datos

En esta parte intentaremos aplicar varios métodos de minería de datos para poder sacar conclusiones a partir de los datos. A continuación se exponen los algoritmos que se utilizarán y cuál es su propósito: * Regresión Lineal : el objetivo de este algoritmo es predecir un valor continuo numérico (variable dependiente Y) según otras variables (variables independientes Xs). En este caso, es interesante predecir el atributo Rating en función de otros.

Regresión Logística

En primer lugar haremos el modelo con validación train-test

##https://www.analyticsvidhya.com/blog/2014/12/caret-package-stop-solution-building-predictive-models/

#http://r-statistics.co/Linear-Regression.html
library(dummies)
## Warning: package 'dummies' was built under R version 3.5.2
## dummies-1.5.6 provided by Decision Patterns
library(caret)
## Warning: package 'caret' was built under R version 3.5.2
## Loading required package: lattice
#One hot encoding
datosAu <- dummy.data.frame(datos, names = c("Category", "Type", "Content.Rating", "Pri_Genre"), sep='.')


#Preparar el conjunto de datos eliminando las columnas que no interesan
borrar <- c("App","Category", "Type", "Content.Rating", "Pri_Genre", "Sec_Genre", "Current.Ver", "Android.Ver", "Last.Updated")
datosRegresion <- datosAu[ , !(names(datosAu) %in% borrar)]

#Dividir el conjunto de datos en train y test
set.seed(3456)
trainIndex <- createDataPartition(datosRegresion$Rating, p = .8, 
                                  list = FALSE, 
                                  times = 1)
datosRegresionTrain <- datosRegresion[ trainIndex,]
datosRegresionTest  <- datosRegresion[-trainIndex,]


#Modelo Regresión Lineal Validación Train Test
lmFit<-train(Rating~., data = datosRegresionTrain, method = "lm")
## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading
## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading
pred <- predict(lmFit, datosRegresionTest)
## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading
modelvalues<-data.frame(obs = datosRegresionTest$Rating, pred=pred)

Validación Train Test. El p-value (p-value: <0.0000000000000002) es mucho menor que el valor de significancia, por lo que los datos son estadísticamente buenos.

summary <- summary(lmFit)
print(defaultSummary(modelvalues))
##     RMSE Rsquared      MAE 
##  0.46321  0.02703  0.31372

Otra cosa interesante que se puede hacer cuando se tiene un modelo de regresión es ver qué variable influye más en la variable que se quiere predecir, en este caso, se observa que el tamaño de la app influye mucho en la puntuación final de la app, así como que la app sea de citas o no en segundo lugar

importancia <- varImp(lmFit)
plot(importancia)

En segundo lugar haremos la validación con K-Cross Validation con k = 10

ctrl<-trainControl(method = "cv",number = 10)

lmCVFit<-train(Rating ~ ., data = datosRegresion, method = "lm", trControl = ctrl, metric="RMSE")
## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading

## Warning in predict.lm(modelFit, newdata): prediction from a rank-deficient
## fit may be misleading
sum<-summary(lmCVFit)
print(sum$r.squared)
## [1] 0.04064

Se observa que con CV se obtiene un RSquared mayor, lo que significa que el resultado de la predicción es mejor.